Advance Types
Literal Types
In TypeScript, Literal Types allow you to specify exact values that a variable can hold, rather than just a general type like string or number.
- A
stringtype means any string ("hello","world","foo"). - A
string literal typemeans only one specific string (like"hello").
This is extremely useful when you want to restrict values to a limited set of possibilities.
let direction: "north" | "south" | "east" | "west";
direction = "north"; // ✅ allowed
direction = "south"; // ✅ allowed
direction = "up"; // ❌ Error: Type '"up"' is not assignable
Type Narrowing
In TypeScript, narrowing means reducing a broad type (like string | number) into a more specific type at runtime based on some checks.
function printLength(value: string | number) {
if (typeof value === "string") {
// TypeScript knows value is a string here
console.log(value.length);
} else {
// Here, value must be a number
console.log(value.toFixed(2));
}
}
Ways to Narrow Types
There are several techniques:
typeofchecksinstanceofchecks- Custom Type Guards (
value is Type)
Narrowing with typeof
The typeof operator is used to check primitive types (string, number, boolean, bigint, symbol, undefined, and object).
function padLeft(value: string, padding: string | number) {
if (typeof padding === "number") {
return " ".repeat(padding) + value; // padding is number
} else {
return padding + value; // padding is string
}
}
console.log(padLeft("Hello", 4)); // " Hello"
console.log(padLeft("Hello", ">> ")); // ">> Hello"
typeofworks great for primitives.- It won’t help for objects or classes — that’s where
instanceofcomes in.
Narrowing with instanceof
instanceof is used to check whether an object is created from a particular class or constructor function.
class Dog {
bark() {
console.log("Woof!");
}
}
class Cat {
meow() {
console.log("Meow!");
}
}
function makeSound(animal: Dog | Cat) {
if (animal instanceof Dog) {
animal.bark(); // animal is narrowed to Dog
} else {
animal.meow(); // animal is narrowed to Cat
}
}
makeSound(new Dog()); // Woof!
makeSound(new Cat()); // Meow!
- Use
instanceofwhen dealing with classes and objects. - It doesn’t work for primitives (use
typeofinstead).
Narrowing with Type Guards (Custom Functions)
Sometimes, you need your own checks for more complex types.
A type guard is a function that returns a type predicate (param is Type).
type Fish = { swim: () => void };
type Bird = { fly: () => void };
function isFish(animal: Fish | Bird): animal is Fish {
return (animal as Fish).swim !== undefined;
}
function move(animal: Fish | Bird) {
if (isFish(animal)) {
animal.swim(); // animal is narrowed to Fish
} else {
animal.fly(); // animal is narrowed to Bird
}
}
isFishis a user-defined type guard.- The
animal is Fishreturn type tells TypeScript that when this function returnstrue, the variable is of typeFish.
Mapped Types
A Mapped Type lets you create a new type by transforming each property of an existing type according to some rule.
Think of it as: Take a type → iterate over its keys → create a new type with modified properties.
type OptionsFlags<Type> = {
[Property in keyof Type]: boolean;
};
keyof Type→ gets all property names ofTypeas a union.Property in keyof Type→ iterate over each key.- The mapped type assigns new property types.
Basic Mapped Types
type Features = {
darkMode: () => void;
multiLanguage: () => void;
};
type FeatureFlags = {
[K in keyof Features]: boolean;
};
// Equivalent to:
type FeatureFlagsManual = {
darkMode: boolean;
multiLanguage: boolean;
};
Every property of Features got transformed into boolean.
Built-in Mapped Types (Readonly, Partial, Required)
TypeScript provides some utility types built using mapped types.
ReadOnly
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
type User = {
id: number;
name: string;
};
type ReadonlyUser = Readonly<User>;
// Equivalent:
type ReadonlyUserManual = {
readonly id: number;
readonly name: string;
};
Partial
type Partial<T> = {
[P in keyof T]?: T[P];
};
type UserPartial = Partial<User>;
// Equivalent:
type UserPartialManual = {
id?: number;
name?: string;
};
All properties are now optional.
Required
type Required<T> = {
[P in keyof T]-?: T[P];
};
type UserOptional = {
id?: number;
name?: string;
};
type UserRequired = Required<UserOptional>;
// Equivalent:
type UserRequiredManual = {
id: number;
name: string;
};
- The
-?removes the optional modifier. - So all properties are required.
Remapping Keys
You can also remap keys using as.
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
type Person = {
name: string;
age: number;
};
type PersonGetters = Getters<Person>;
// Equivalent:
type PersonGettersManual = {
getName: () => string;
getAge: () => number;
};
- Each key is transformed into a new key (
getName,getAge). - Each value becomes a function returning the original property type.
Conditional Types
A Conditional Type looks like this:
T extends U ? X : Y
Meaning:
- If
Tcan be assigned toU→ returnX - Otherwise → return
Y
This is checked at compile-time, not at runtime.
Basic Conditional Type
type IsString<T> = T extends string ? "Yes" : "No";
type A = IsString<string>; // "Yes"
type B = IsString<number>; // "No"
You can use this to create type-level conditions.
Extracting Return Types
type GetReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
type A = GetReturnType<() => number>; // number
type B = GetReturnType<(x: string) => boolean>; // boolean
type C = GetReturnType<string>; // never
infer R→ captures the return type of the function.- If
Tis a function, return its return type. Otherwise →never. This is how TypeScript’s built-inReturnType<T>works.
Conditional Type with Unions (Distributive Behavior)
Conditional types are distributive over unions.
type ToArray<T> = T extends any ? T[] : never;
type A = ToArray<number>; // number[]
type B = ToArray<number | string>; // number[] | string[]
When you pass number | string, TypeScript distributes: ToArray<number> | ToArray<string> → number[] | string[].
This is powerful, but sometimes you don’t want distributive behavior → you can wrap in square brackets:
type ToArrayNonDistributive<T> = [T] extends [any] ? T[] : never;
type C = ToArrayNonDistributive<number | string>; // (number | string)[]
Filtering Types
You can use conditional types to filter properties.
type Exclude<T, U> = T extends U ? never : T;
type A = Exclude<"a" | "b" | "c", "a">; // "b" | "c"
- ``"a"
extends"a"→ becomesnever` → excluded. - ``"b"
and"c"` remain.
Conditional Type with infer
The infer keyword lets you extract a type inside a conditional.
type FirstElement<T> = T extends [infer U, ...any[]] ? U : never;
type A = FirstElement<[string, number, boolean]>; // string
type B = FirstElement<[]>; // never
- If
Tis a tuple, extract the first element type. - If not, return
never.
Utility Types
Partial<T>
Makes all properties optional.
type User = {
id: number;
name: string;
age: number;
};
type PartialUser = Partial<User>;
// Equivalent:
type PartialUserManual = {
id?: number;
name?: string;
age?: number;
};
Required<T>
Makes all properties required (opposite of Partial).
type UserOptional = {
id?: number;
name?: string;
};
type UserRequired = Required<UserOptional>;
// Equivalent:
type UserRequiredManual = {
id: number;
name: string;
};
Readonly<T>
Makes all properties read-only.
type User = {
id: number;
name: string;
};
type ReadonlyUser = Readonly<User>;
// Equivalent:
type ReadonlyUserManual = {
readonly id: number;
readonly name: string;
};
const user: ReadonlyUser = { id: 1, name: "Alice" };
user.id = 2; // ❌ Error: Cannot assign to 'id'
Pick<T, K>
Creates a type by picking a subset of properties.
type User = {
id: number;
name: string;
age: number;
};
type UserPreview = Pick<User, "id" | "name">;
// Equivalent:
type UserPreviewManual = {
id: number;
name: string;
};
Omit<T, K>
Creates a type by removing specific properties.
type User = {
id: number;
name: string;
age: number;
};
type UserWithoutAge = Omit<User, "age">;
// Equivalent:
type UserWithoutAgeManual = {
id: number;
name: string;
};
Record<K, T>
Constructs a type with keys K and values T.
type Roles = "admin" | "editor" | "viewer";
type RolePermissions = Record<Roles, boolean>;
// Equivalent:
type RolePermissionsManual = {
admin: boolean;
editor: boolean;
viewer: boolean;
};
const permissions: RolePermissions = {
admin: true,
editor: false,
viewer: true,
};
Exclude<T, U>
Excludes types from a union.
type Letters = "a" | "b" | "c";
type WithoutA = Exclude<Letters, "a">;
// "b" | "c"
Extract<T, U>
Extracts types that are assignable to U.
type Letters = "a" | "b" | "c";
type OnlyA = Extract<Letters, "a" | "d">;
// "a"
Opposite of Exclude.